package com.nn.algorithm;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Network {

	private List<Layer> layers;
	private boolean trainingMode = false;

	private Network(boolean trainingMode) {
		layers = new ArrayList<>();
		this.trainingMode = trainingMode;
	}

	public static Network createNetworkInClassificationMode() {

		return new Network(false);
	}

	public static Network createNetworkInTrainingMode() {

		return new Network(true);
	}

	public void addLayer(Layer layer) {
		this.layers.add(layer);
		if (!trainingMode && !layer.getLayerType().equals(LayerType.OUTPUT)) {
			layer.readWeightMatrix(this.layers.indexOf(layer));
		}
	}

	public Layer getLayer(int index) {
		return this.layers.get(index);
	}

	public List<Layer> getLayers() {
		return this.layers;
	}

	public double[] getNetworkOutput(double[] input) {
		for (Layer layer : layers) {
			if (layer.getLayerType() == LayerType.INPUT) {
				layer.processLayer(input);
			} else {
				layer.processLayer(null);
			}
		}
		return layers.get(layers.size() - 1).getOutput();
	}

	/**
	 * Rounded output, should be used when network is trained
	 * 
	 * @param input
	 * @return
	 */
	public double[] calculateRoundedOutput(double[] input) {
		double[] output = getNetworkOutput(input);
		double max = Double.MIN_VALUE;
		for (int i = 0; i < output.length; i++) {
			if (output[i] > max) {
				max = output[i];
			}
		}
		for (int i = 0; i < output.length; i++) {
			if (output[i] == max) {
				output[i] = 1.0;
			} else {
				output[i] = 0.0;
			}
		}
		return output;
	}

	// RMS error
	public double calculateError(double[][] input, double[][] idealOutput) {
		double error = 0;
		double n = 0;
		for (int i = 0; i < input.length; i++) {
			double[] output = this.getNetworkOutput(input[i]);
			for (int j = 0; j < output.length; j++) {
				error += Math.pow(idealOutput[i][j] - output[j], 2);
			}
			n += input.length;
		}
		return Math.sqrt(error / n);
	}

	/**
	 * Build a network with one hidden layer.
	 * @param trainingMode True if the network is to be trained, false for classification
	 * @return the network
	 */
	public static Network buildNetworkWithOneHiddenLayer(boolean trainingMode) {

		Network network;
		if (trainingMode) {
			network = Network.createNetworkInTrainingMode();
		} else {
			network = Network.createNetworkInClassificationMode();
		}
		Layer input_layer = new Layer(72, LayerType.INPUT);
		Layer hidden_layer = new Layer(51, LayerType.HIDDEN); // rule of thumb: 2/3 input + output
		Layer output_layer = new Layer(3, LayerType.OUTPUT);
		input_layer.setNext(hidden_layer);
		hidden_layer.setPrevious(input_layer);
		hidden_layer.setNext(output_layer);
		output_layer.setPrevious(hidden_layer);
		input_layer.init();
		hidden_layer.init();
		output_layer.init();
		network.addLayer(input_layer);
		network.addLayer(hidden_layer);
		network.addLayer(output_layer);
		return network;
	}

	public static Network buildNetworkWithOneHiddenLayer_XOR_TEST() {
		Network network = Network.createNetworkInTrainingMode();
		Layer input_layer = new Layer(2, LayerType.INPUT);
		Layer hidden_layer = new Layer(3, LayerType.HIDDEN);
		Layer output_layer = new Layer(1, LayerType.OUTPUT);
		input_layer.setNext(hidden_layer);
		hidden_layer.setPrevious(input_layer);
		hidden_layer.setNext(output_layer);
		output_layer.setPrevious(hidden_layer);
		input_layer.init();
		hidden_layer.init();
		output_layer.init();
		network.addLayer(input_layer);
		network.addLayer(hidden_layer);
		network.addLayer(output_layer);
		return network;
	}

	public static void main(String[] args) {

		double input[] = { 1.0, 2.0 };
		Network network = Network.buildNetworkWithOneHiddenLayer(false);
		network.getLayer(0).processLayer(input);
		network.getLayer(1).processLayer(null);
		network.getLayer(2).processLayer(null);
		System.out.println(Arrays.toString(input));
		System.out.println(Arrays.toString(network.getLayer(0).getOutput()));
		System.out.println(Arrays.toString(network.getLayer(1).getOutput()));
		System.out.println(Arrays.toString(network.getLayer(2).getOutput()));
		System.out.println(Arrays.toString(network.getNetworkOutput(input)));
	}

}
